package com.dbflow5.processor.definition.column;

import com.dbflow5.StringUtils;
import com.dbflow5.annotation.*;
import com.dbflow5.processor.ClassNames;
import com.dbflow5.processor.ProcessorManager;
import com.dbflow5.processor.Validators;
import com.dbflow5.processor.definition.EntityDefinition;
import com.dbflow5.processor.definition.QueryModelDefinition;
import com.dbflow5.processor.definition.TableDefinition;
import com.dbflow5.processor.definition.behavior.ColumnBehaviors;
import com.dbflow5.processor.utils.ElementExtensions;
import com.dbflow5.processor.utils.ProcessorUtils;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.NameAllocator;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;

/**
 * Description: Represents both a [ForeignKey] and [ColumnMap]. Builds up the model of fields
 * required to generate definitions.
 */
public class ReferenceColumnDefinition extends ColumnDefinition{

    /**
     * If null, its a [ColumnMap]
     */
    public ColumnBehaviors.ForeignKeyColumnBehavior foreignKeyColumnBehavior;

    /**
     * Foreign key references. If exists, it's precomputed first.
     */
    private final List<ReferenceSpecificationDefinition> references;

    /**
     * If true, full model object does not load on cursor load.
     */
    private final boolean isStubbedRelationship;

    public ReferenceColumnDefinition(ProcessorManager manager, EntityDefinition tableDefinition,
                                     Element element, boolean isPackagePrivate, ColumnBehaviors.ForeignKeyColumnBehavior foreignKeyColumnBehavior,
                                     List<ReferenceSpecificationDefinition> references, boolean isStubbedRelationship) {
        super(manager, element, tableDefinition, isPackagePrivate, element.getAnnotation(Column.class), element.getAnnotation(PrimaryKey.class), ConflictAction.NONE);
        this.foreignKeyColumnBehavior = foreignKeyColumnBehavior;
        this.references = references;
        this.isStubbedRelationship = isStubbedRelationship;

        explicitReferences = !references.isEmpty();

        if (isNotNullType) {
            manager.logError("Foreign Keys must be nullable. Please remove the non-null annotation if using " +
                    "Java, or add ? to the type for Kotlin.");
        }
    }

    private final List<ReferenceDefinition> _referenceDefinitionList = new ArrayList<>();
    public List<ReferenceDefinition> referenceDefinitionList() {
        checkNeedsReferences();
        return _referenceDefinitionList;
    }

    public ClassName referencedClassName = null;

    public boolean isReferencingTableObject = false;

    private boolean implementsModel;
    private boolean extendsBaseModel;
    private boolean nonModelColumn;

    public boolean isColumnMap() {
        return foreignKeyColumnBehavior == null;
    }

    private boolean needsReferences = true;
    public boolean explicitReferences;

    @Override
    public List<TypeName> typeConverterElementNames() {
        Set<TypeName> uniqueTypes = new LinkedHashSet<>();

        referenceDefinitionList().forEach(referenceDefinition -> {
            if(referenceDefinition.hasTypeConverter()) {
                uniqueTypes.add(referenceDefinition.columnClassName);
            }
        });

        return new ArrayList<>(uniqueTypes);
    }

    public ReferenceColumnDefinition (ColumnMap columnMap, ProcessorManager manager, EntityDefinition tableDefinition, Element element, boolean isPackagePrivate)  {
        this(manager, tableDefinition, element, isPackagePrivate, null,
                Arrays.stream(columnMap.references()).map(reference -> {
                    TypeMirror typeMirror = ProcessorUtils.extractTypeMirrorFromAnnotation(reference, e -> null, it -> {
                        it.typeConverter();
                        return null;
                    });
                    ClassName typeConverterClassName = typeMirror != null? ProcessorUtils.fromTypeMirror(typeMirror, manager) : null;
                    return new ReferenceSpecificationDefinition(reference.columnName(), reference.columnMapFieldName(),
                            reference.notNull().onNullConflict(), reference.defaultValue(),
                            typeConverterClassName, typeMirror);
                }).collect(Collectors.toList()),
        // column map is always stubbed
        true
        );
        findReferencedClassName(manager);

        // self create a column map if defined here.
        if(typeElement != null) {
            manager.addQueryModelDefinition(new QueryModelDefinition(typeElement, tableDefinition.associationalBehavior().databaseTypeName, manager));
        }
    }

    public ReferenceColumnDefinition(ForeignKey foreignKey, ProcessorManager manager, EntityDefinition tableDefinition,
                                     Element element, boolean isPackagePrivate) {
        this(manager, tableDefinition, element, isPackagePrivate,
                new ColumnBehaviors.ForeignKeyColumnBehavior(foreignKey.onDelete(), foreignKey.onUpdate(), foreignKey.saveForeignKeyModel(),
                        foreignKey.deleteForeignKeyModel(), foreignKey.deferred()),
                Arrays.stream(foreignKey.references()).map(reference -> new ReferenceSpecificationDefinition(reference.columnName(), reference.foreignKeyColumnName(),
                        reference.notNull().onNullConflict(), reference.defaultValue(), null, null)).collect(Collectors.toList()),
        foreignKey.stubbedRelationship()
        );
        if (!(tableDefinition instanceof TableDefinition)) {
            manager.logError("Class "+elementName+" cannot declare a @ForeignKey. Use @ColumnMap instead.");
        }

        TypeMirror typeMirror = ProcessorUtils.extractTypeMirrorFromAnnotation(foreignKey, e -> null, it -> {
            it.tableClass();
            return null;
        });

        if(typeMirror != null) {
            referencedClassName = ProcessorUtils.fromTypeMirror(typeMirror, manager);
        }

        // hopefully intentionally left blank
        if (referencedClassName == TypeName.OBJECT) {
            findReferencedClassName(manager);
        }

        if (referencedClassName == null) {
            manager.logError("Referenced was null for "+element+" within "+elementTypeName);
        }

        TypeElement erasedElement = ElementExtensions.toTypeErasedElement(element, ProcessorManager.manager);
        extendsBaseModel = ProcessorUtils.isSubclass(erasedElement, manager.processingEnvironment, ClassNames.BASE_MODEL);
        implementsModel = ProcessorUtils.implementsClass(erasedElement, manager.processingEnvironment, ClassNames.MODEL);
        isReferencingTableObject = implementsModel || erasedElement.getAnnotation(Table.class) != null;

        nonModelColumn = !isReferencingTableObject;
    }

    private void findReferencedClassName(ProcessorManager manager) {
        if (elementTypeName instanceof ParameterizedTypeName) {
            List<TypeName> args = ((ParameterizedTypeName) elementTypeName).typeArguments;
            if (args.size() > 0) {
                referencedClassName = ClassName.get(ElementExtensions.toTypeElement(args.get(0), manager));
            }
        } else {
            if (referencedClassName == null || referencedClassName == ClassName.OBJECT) {
                referencedClassName = ElementExtensions.toClassName(ElementExtensions.toTypeElement(elementTypeName, ProcessorManager.manager), ProcessorManager.manager);
            }
        }
    }

    @Override
    public void addPropertyDefinition(TypeSpec.Builder typeBuilder, TypeName tableClass) {
        referenceDefinitionList().forEach(referenceDefinition -> {
            TypeName propParam = null;
            TypeName colClassName = referenceDefinition.columnClassName;
            if(colClassName != null) {
                propParam = ParameterizedTypeName.get(ClassNames.PROPERTY, colClassName.box());
            }
            if (StringUtils.isNullOrEmpty(referenceDefinition.columnName())) {
                manager.logError("Found empty reference name at " + referenceDefinition.foreignColumnName +
                        " from table " + entityDefinition.elementName);
            }
            typeBuilder.addField(FieldSpec.builder(propParam, referenceDefinition.columnName(),
                    Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                    .initializer("new $T($T.class, $S)", propParam, tableClass, referenceDefinition.columnName())
                    .addJavadoc(
                            isColumnMap()? "Column Mapped Field"
            : ("Foreign Key" + (type == Type.Primary.INSTANCE? " / Primary Key" : ""))).build());
        });
    }

    @Override
    public void addPropertyCase(MethodSpec.Builder methodBuilder) {
        referenceDefinitionList().forEach(it -> {
            methodBuilder.beginControlFlow("case \"" + StringUtils.quoteIfNeeded(it.columnName())+"\"").addStatement("return " + it.columnName()).endControlFlow();
        });
    }

    @Override
    public void appendIndexInitializer(CodeBlock.Builder initializer, AtomicInteger index) {
        if (nonModelColumn) {
            super.appendIndexInitializer(initializer, index);
        } else {
            referenceDefinitionList().forEach(it -> {
                if (index.get() > 0) {
                    initializer.add(", ");
                }
                initializer.add(it.columnName());
                index.incrementAndGet();
            });
        }
    }

    @Override
    public void addColumnName(CodeBlock.Builder codeBuilder) {
        List<ReferenceDefinition> list = referenceDefinitionList();
        for(int i = 0;i<list.size();i++) {
            ReferenceDefinition reference = list.get(i);
            if (i > 0) {
                codeBuilder.add(",");
            }
            codeBuilder.add(reference.columnName());
        }
    }

    @Override
    public CodeBlock updateStatementBlock() {
        CodeBlock.Builder builder = CodeBlock.builder();
        List<ReferenceDefinition> list = referenceDefinitionList();
        for(int i=0;i<list.size();i++) {
            ReferenceDefinition referenceDefinition = list.get(i);
            if (i > 0) {
                builder.add(",");
            }
            builder.add(CodeBlock.of(StringUtils.quote(referenceDefinition.columnName()) + "=?"));
        }
        return builder.build();
    }

    @Override
    public CodeBlock insertStatementColumnName() {
        CodeBlock.Builder builder = CodeBlock.builder();
        List<ReferenceDefinition> list = referenceDefinitionList();
        for(int i=0;i<list.size();i++) {
            ReferenceDefinition referenceDefinition = list.get(i);
            if (i > 0) {
                builder.add(",");
            }
            builder.add(StringUtils.quote(referenceDefinition.columnName()));
        }
        return builder.build();
    }

    @Override
    public CodeBlock insertStatementValuesString() {
        CodeBlock.Builder builder = CodeBlock.builder();
        List<ReferenceDefinition> list = referenceDefinitionList();

        for(int i=0;i<list.size();i++) {
            if (i > 0) {
                builder.add(",");
            }
            builder.add("?");
        }
        return builder.build();
    }

    @Override
    public CodeBlock creationName() {
        CodeBlock.Builder builder = CodeBlock.builder();
        List<ReferenceDefinition> list = referenceDefinitionList();
        for(int i=0;i<list.size();i++) {
            ReferenceDefinition referenceDefinition = list.get(i);
            if (i > 0) {
                builder.add(" ,");
            }
            builder.add(referenceDefinition.creationStatement());

            if (referenceDefinition.notNull()) {
                builder.add(" NOT NULL ON CONFLICT $L", referenceDefinition.onNullConflict);
            } else if (!explicitReferences && notNull) {
                builder.add(" NOT NULL ON CONFLICT $L", onNullConflict);
            }
        }
        return builder.build();
    }

    @Override
    public String primaryKeyName() {
        CodeBlock.Builder builder = CodeBlock.builder();
        List<ReferenceDefinition> list = referenceDefinitionList();

        for(int i=0;i<list.size();i++) {
            ReferenceDefinition referenceDefinition = list.get(i);
            if (i > 0) {
                builder.add(" ,");
            }
            builder.add(referenceDefinition.primaryKeyName());
        }
        return builder.build().toString();
    }

    @Override
    public CodeBlock contentValuesStatement() {
        if (nonModelColumn) {
            return super.contentValuesStatement();
        } else {
            CodeBlock.Builder builder = CodeBlock.builder();
            if(referencedClassName != null) {
                ForeignKeyAccessCombiner foreignKeyCombiner = new ForeignKeyAccessCombiner(columnAccessor);

                referenceDefinitionList().forEach(it -> {
                    foreignKeyCombiner.fieldAccesses.add(it.contentValuesField);
                });
                foreignKeyCombiner.addCode(builder, new AtomicInteger(0), true, true);
            }
            return builder.build();
        }
    }

    @Override
    public CodeBlock getSQLiteStatementMethod(AtomicInteger index, boolean defineProperty) {
        if (nonModelColumn) {
            return super.getSQLiteStatementMethod(index, defineProperty);
        } else {
            CodeBlock.Builder codeBuilder = CodeBlock.builder();
            if(referencedClassName != null) {
                ForeignKeyAccessCombiner foreignKeyCombiner = new ForeignKeyAccessCombiner(columnAccessor);
                referenceDefinitionList().forEach(it -> {
                    foreignKeyCombiner.fieldAccesses.add(it.sqliteStatementField);
                });
                foreignKeyCombiner.addCode(codeBuilder, index, defineProperty, true);
            }

            return codeBuilder.build();
        }
    }

    @Override
    public CodeBlock getLoadFromCursorMethod(boolean endNonPrimitiveIf, AtomicInteger index, NameAllocator nameAllocator) {
        if (nonModelColumn) {
            return super.getLoadFromCursorMethod(endNonPrimitiveIf, index, nameAllocator);
        } else {
            CodeBlock.Builder code = CodeBlock.builder();
            if(referencedClassName != null) {
                EntityDefinition tableDefinition = manager.getReferenceDefinition(entityDefinition.databaseDefinition().elementTypeName, referencedClassName);
                if(tableDefinition != null && tableDefinition.outputClassName != null) {
                    ForeignKeyAccessCombiner.ForeignKeyLoadFromCursorCombiner foreignKeyCombiner = new ForeignKeyAccessCombiner.ForeignKeyLoadFromCursorCombiner(columnAccessor,
                            referencedClassName, outputClassName, isStubbedRelationship, nameAllocator);
                    referenceDefinitionList().forEach(it -> foreignKeyCombiner.fieldAccesses.add(it.partialAccessor));
                    foreignKeyCombiner.addCode(code, index);
                }
            }
            return code.build();
        }
    }

    @Override
    public void appendPropertyComparisonAccessStatement(CodeBlock.Builder codeBuilder) {
        if(nonModelColumn) {
            new ColumnAccessCombiner.PrimaryReferenceAccessCombiner(combiner).addCode(codeBuilder, referenceDefinitionList().get(0).columnName(), getDefaultValueBlock(), 0, ColumnAccessor.modelBlock, false);
        }else if(columnAccessor instanceof ColumnAccessor.TypeConverterScopeColumnAccessor) {
            super.appendPropertyComparisonAccessStatement(codeBuilder);
        }else {
            if(referencedClassName != null) {
                ForeignKeyAccessCombiner foreignKeyCombiner = new ForeignKeyAccessCombiner(columnAccessor);
                referenceDefinitionList().forEach(referenceDefinition -> foreignKeyCombiner.fieldAccesses.add(referenceDefinition.primaryReferenceField));
                foreignKeyCombiner.addCode(codeBuilder, new AtomicInteger(0), true, true);
            }
        }
    }

    public void appendSaveMethod(CodeBlock.Builder codeBuilder) {
        if (!nonModelColumn && !(columnAccessor instanceof ColumnAccessor.TypeConverterScopeColumnAccessor)) {
            if(referencedClassName != null) {
                ForeignKeyAccessCombiner.ForeignKeyAccessField saveAccessor = new ForeignKeyAccessCombiner.ForeignKeyAccessField(columnName,
                        new ColumnAccessCombiner.SaveModelAccessCombiner(new ColumnAccessCombiner.Combiner(columnAccessor, referencedClassName,
                                complexColumnBehavior.wrapperAccessor,
                                complexColumnBehavior.wrapperTypeName,
                                complexColumnBehavior.subWrapperAccessor, ""), implementsModel, extendsBaseModel), null);
                saveAccessor.addCode(codeBuilder, 0, ColumnAccessor.modelBlock, true, true);
            }
        }
    }

    public void appendDeleteMethod(CodeBlock.Builder codeBuilder) {
        if (!nonModelColumn && !(columnAccessor instanceof ColumnAccessor.TypeConverterScopeColumnAccessor)) {
            if(referencedClassName != null) {
                ForeignKeyAccessCombiner.ForeignKeyAccessField deleteAccessor = new ForeignKeyAccessCombiner.ForeignKeyAccessField(columnName,
                        new ColumnAccessCombiner.DeleteModelAccessCombiner(new ColumnAccessCombiner.Combiner(columnAccessor, referencedClassName,
                                complexColumnBehavior.wrapperAccessor,
                                complexColumnBehavior.wrapperTypeName,
                                complexColumnBehavior.subWrapperAccessor, ""), implementsModel, extendsBaseModel), null);
                deleteAccessor.addCode(codeBuilder, 0, ColumnAccessor.modelBlock, true, true);
            }
        }
    }

    /**
     * If [ForeignKey] has no [ForeignKeyReference]s, we use the primary key the referenced
     * table. We do this post-evaluation so all of the [TableDefinition] can be generated.
     */
    public void checkNeedsReferences() {
        EntityDefinition referencedTableDefinition = manager.getReferenceDefinition(entityDefinition.associationalBehavior().databaseTypeName, referencedClassName);
        if (referencedTableDefinition == null) {
            throwCannotFindReference();
        } else if (needsReferences) {
            List<ColumnDefinition> primaryColumns = isColumnMap()? referencedTableDefinition.columnDefinitions : referencedTableDefinition.primaryColumnDefinitions();
            if (references.isEmpty()) {
                primaryColumns.forEach(columnDefinition -> {
                    TypeMirror typeMirror = columnDefinition.column != null? ProcessorUtils.extractTypeMirrorFromAnnotation(columnDefinition.column, e -> null, it -> {
                        it.typeConverter();
                        return null;
                    }) : null;
                    ClassName typeConverterClassName = typeMirror != null? ProcessorUtils.fromTypeMirror(typeMirror, manager) : null;
                    ReferenceDefinition referenceDefinition = new ReferenceDefinition(manager,
                            elementName,
                            columnDefinition.elementName,
                            columnDefinition,
                            this,
                            primaryColumns.size(),
                            isColumnMap()? columnDefinition.elementName : "",
                            ConflictAction.NONE,
                            defaultValue = columnDefinition.defaultValue,
                            typeConverterClassName,
                            typeMirror
                    );
                    _referenceDefinitionList.add(referenceDefinition);
                });

                needsReferences = false;
            } else {
                references.forEach(reference -> {
                    ColumnDefinition foundDefinition = null;
                    for(ColumnDefinition definition : primaryColumns) {
                        if(definition.columnName.equals(reference.referenceName)) {
                            foundDefinition = definition;
                            break;
                        }
                    }
                    if (foundDefinition == null) {
                        manager.logError(ReferenceColumnDefinition.class,
                        "Could not find referenced column "+reference.referenceName+" " +
                                "from reference named "+reference.columnName);
                    } else {
                        _referenceDefinitionList.add(
                                new ReferenceDefinition(manager,
                                        elementName,
                                        foundDefinition.elementName,
                                        foundDefinition,
                                        this,
                                        primaryColumns.size(),
                                        reference.columnName,
                                        reference.onNullConflictAction,
                                        reference.defaultValue,
                                        reference.typeConverterClassName,
                                        reference.typeConverterTypeMirror
                                ));
                    }
                });

                needsReferences = false;
            }

            if (nonModelColumn && _referenceDefinitionList.size() == 1) {
                columnName = _referenceDefinitionList.get(0).columnName();
            }

            _referenceDefinitionList.forEach(it -> {
                if (it.columnClassName != null && it.columnClassName.isPrimitive()
                        && !StringUtils.isNullOrEmpty(it.defaultValue)) {
                    manager.logWarning(Validators.ColumnValidator.class,
                            "Default value of \""+it.defaultValue+"\" from " +
                                    ""+entityDefinition.elementName+"."+elementName+" is ignored for primitive columns.");
                }
            });
        }
    }

    public void throwCannotFindReference() {
        manager.logError(ReferenceColumnDefinition.class,
            "Could not find the referenced ${Table::class.java.simpleName} " +
                "or ${QueryModel::class.java.simpleName} definition $referencedClassName" +
                " from ${entityDefinition.elementName}. " +
                "Ensure it exists in the same database as ${entityDefinition.associationalBehavior.databaseTypeName}");
    }

    public static class ReferenceSpecificationDefinition{
        public String columnName;
        public String referenceName;
        public ConflictAction onNullConflictAction;
        public String defaultValue;
        public ClassName typeConverterClassName;
        public TypeMirror typeConverterTypeMirror;

        public ReferenceSpecificationDefinition(String columnName, String referenceName, ConflictAction onNullConflictAction, String defaultValue, ClassName typeConverterClassName, TypeMirror typeConverterTypeMirror) {
            this.columnName = columnName;
            this.referenceName = referenceName;
            this.onNullConflictAction = onNullConflictAction;
            this.defaultValue = defaultValue;
            this.typeConverterClassName = typeConverterClassName;
            this.typeConverterTypeMirror = typeConverterTypeMirror;
        }
    }
}

